contents
1. 개요 및 탄생 배경
- 헥사고날 아키텍처(Ports and Adapters)는 2000년대 초 알리스테어 코크번(Alistair Cockburn)이 제안한 소프트웨어 설계 패턴입니다.
- 육각형 모양으로 표현되는 이유는 인터페이스 경계를 시각적으로 나타내기 위한 것이지, 꼭 6개의 인터페이스만 사용한다는 뜻은 아닙니다.
2. 핵심 원칙
- 애플리케이션 코어(핵심 비즈니스 로직) 분리: 비즈니스 규칙과 도메인 로직이 중심(내부 영역, 육각형 중앙)에 위치하며 데이터베이스, UI, 메시지 큐, 외부 프레임워크 등과 격리되어 있습니다.
- 포트(Port): 애플리케이션이 외부와 상호작용하는 경계/인터페이스입니다.
- 드라이버 포트(Primary Port): 외부에서 애플리케이션을 호출(예: REST, CLI, 스케줄러).
- 드리븐 포트(Secondary Port): 애플리케이션 코어가 외부 시스템(DB, 메시지, 외부 API 등)과 상호작용할 때 사용하는 인터페이스.
- 어댑터(Adapter): 포트를 구현하며, 각종 기술의 세부사항을 처리(예: HTTP 요청을 도메인 모델로 변환, JPA DB 연동 등).
- 드라이버 어댑터: 외부 요청을 애플리케이션 내부 로직으로 전달.
- 드리븐 어댑터: 코어가 특정 기술을 사용할 수 있도록 변환(예: DB, 외부 API).
3. 구조와 분리
구조 시각화
+--------------------------+
| 드라이버 어댑터 |
+--------------------------+
| 드라이버 포트 |
+--------------------------+
[육각형 애플리케이션 코어]
+--------------------------+
| 드리븐 포트 |
+--------------------------+
| 드리븐 어댑터 |
+--------------------------+
- 코어 애플리케이션은 기술, 라이브러리, 프레임워크에 전혀 의존하지 않음. 모든 통신은 추상 포트(인터페이스)로만 이루어집니다.
- 어댑터는 코어를 감싸 외부 프로토콜/포맷과 도메인 객체 간 변환을 담당합니다.
4. 주요 장점
- 테스트 용이성: 코어 로직은 DB, UI, 서버 없이도 단위테스트 가능하며, 모킹된 어댑터로 외부 의존성 없이 검증할 수 있습니다.
- 유연성: 기술(예: RDB→NoSQL, REST→gRPC 등) 변경 시 어댑터만 교체하면 코어는 그대로 유지.
- 결합도 감소: 각 외부 연동(DB, UI, 메시징 등)이 별도의 포트/어댑터로 설계되어 코어와 인프라 계층이 직접 연결되지 않음.
- 유지보수 및 확장: 경계가 명확하기 때문에 리팩토링, 기능 확장, 외부 시스템 교체가 수월해집니다.
5. 일반 구현 단계
- 코어 설계:
- 비즈니스/도메인 로직을 순수 객체/클래스로 작성.
- 포트를 인터페이스로 정의(예:
OrderRepository,EmailSender).
- 드라이버 어댑터 구현:
- 웹 컨트롤러, CLI, 배치 등을 구현하여 드라이버 포트 호출.
- 드리븐 어댑터 구현:
- DB, 외부 API, 메시지 브로커 등 기술 구체 구현을 드리븐 포트에 맞춰서 구현.
- 의존성 주입:
- 어댑터와 포트를 어플리케이션 시작 시 연결(Dependency Injection).
- 코어는 실제 구현체에 대한 정보 없이 인터페이스만 사용.
6. 실제 예시
- 결제 처리 애플리케이션:
- 코어: 비즈니스 서비스(결제 검증, 주문 업데이트 등).
- 포트:
PaymentService(드라이버 포트, UI/API),PaymentRepository(드리븐 포트, DB),EventPublisher(드리븐 포트, 메시지). - 어댑터: REST 컨트롤러(API), JPA 어댑터(DB), Kafka 어댑터(이벤트 발행).
7. 활용 및 고려사항
- 경계가 뚜렷하여 대형/복잡 시스템, 테스트 자동화, 기술변경이 잦은 서비스에 특히 적합.
- 소규모/단순 프로젝트에는 오버엔지니어링일 수 있으니 적절성 판단 필요.
요약:
헥사고날 아키텍처는 핵심 도메인/비즈니스 로직을 모든 외부 기술로부터 완벽하게 분리하여, 포트(인터페이스)와 어댑터(구현체)를 통해만 입출력을 처리합니다. 이 구조는 유연성과 테스트 용이성, 인프라 변화에 대한 견고함을 제공합니다.
이제 간단한 java & spring 프로젝트로 예를 만들어 보겠습니다.
프로젝트 구조 예시
src/main/java/com/example/hexagonal/
├── domain/
│ ├── model/ // 핵심 도메인 모델
│ │ └── Order.java
│ └── ports/ // 도메인 포트(인터페이스)
│ ├── incoming/ // 드라이버 포트 - 애플리케이션 진입점
│ │ └── OrderServicePort.java
│ └── outgoing/ // 드리븐 포트 - 외부 시스템과 통신하는 인터페이스
│ └── OrderRepositoryPort.java
├── application/
│ └── service/ // 드라이버 포트 구현, 유스케이스
│ └── OrderService.java
├── infrastructure/
│ ├── persistence/ // 드리븐 어댑터, DB 구현체
│ │ └── OrderRepositoryImpl.java
│ └── web/ // 드라이버 어댑터, REST 컨트롤러
│ └── OrderController.java
├── HexagonalApplication.java // Spring Boot 메인 클래스
1. 도메인 계층 - 모델 및 포트
// domain/model/Order.java
public class Order {
private Long id;
private String product;
private int quantity;
// 생성자, getter, setter 등
}
// domain/ports/incoming/OrderServicePort.java
import java.util.List;
public interface OrderServicePort {
List<Order> getAllOrders();
Order placeOrder(Order order);
}
// domain/ports/outgoing/OrderRepositoryPort.java
import java.util.List;
public interface OrderRepositoryPort {
List<Order> findAll();
Order save(Order order);
}
2. 애플리케이션 계층 - 유스케이스 구현
// application/service/OrderService.java
import com.example.hexagonal.domain.model.Order;
import com.example.hexagonal.domain.ports.incoming.OrderServicePort;
import com.example.hexagonal.domain.ports.outgoing.OrderRepositoryPort;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderService implements OrderServicePort {
private final OrderRepositoryPort orderRepository;
public OrderService(OrderRepositoryPort orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
@Override
public Order placeOrder(Order order) {
// 비즈니스 로직 추가 가능
return orderRepository.save(order);
}
}
3. 인프라 계층 - 어댑터 구현
// infrastructure/persistence/OrderRepositoryImpl.java
import com.example.hexagonal.domain.model.Order;
import com.example.hexagonal.domain.ports.outgoing.OrderRepositoryPort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface JpaOrderRepository extends JpaRepository<Order, Long> {}
@Repository
public class OrderRepositoryImpl implements OrderRepositoryPort {
private final JpaOrderRepository jpaOrderRepository;
public OrderRepositoryImpl(JpaOrderRepository jpaOrderRepository) {
this.jpaOrderRepository = jpaOrderRepository;
}
@Override
public List<Order> findAll() {
return jpaOrderRepository.findAll();
}
@Override
public Order save(Order order) {
return jpaOrderRepository.save(order);
}
}
// infrastructure/web/OrderController.java
import com.example.hexagonal.domain.model.Order;
import com.example.hexagonal.domain.ports.incoming.OrderServicePort;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderServicePort orderService;
public OrderController(OrderServicePort orderService) {
this.orderService = orderService;
}
@GetMapping
public List<Order> getOrders() {
return orderService.getAllOrders();
}
@PostMapping
public Order createOrder(@RequestBody Order order) {
return orderService.placeOrder(order);
}
}
4. Spring Boot 메인 클래스
// HexagonalApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HexagonalApplication {
public static void main(String[] args) {
SpringApplication.run(HexagonalApplication.class, args);
}
}
요약:
- 도메인은 핵심 비즈니스 모델과 포트(인터페이스)를 정의합니다.
- **애플리케이션(서비스)**은 포트들을 구현하여 유스케이스 로직을 작성합니다.
- 인프라스트럭처는 드리븐 포트에 대한 구체적 구현체(DB, 외부 시스템)를 제공합니다.
- 웹 컨트롤러는 드라이버 어댑터로서 포트를 호출해 애플리케이션과 통신합니다.
이렇게 헥사고날 아키텍처는 핵심 로직과 외부 기술을 완벽히 분리하여 높은 유연성과 테스트 용이성을 제공합니다.
사실 이렇게 봐선 클린 아키텍처와 헥사고날 아키텍처의 차이를 이해하기는 쉽지 않습니다. 솔직히 별로 다르지도 않구요... 좀 더 이해를 돕기 위해 찾아봤습니다.
주요 차이점
1. 구조 및 레이어
-
헥사고날 아키텍처
- "하나의 코어를 포트와 어댑터로 감싼 구조"를 강조합니다.
- 시스템의 핵심(비즈니스 로직)이 중심에 있습니다.
- 모든 통신은 추상화된 '포트'라는 인터페이스를 통해 이루어집니다.
- '어댑터'는 핵심 로직과 외부 세계(웹, DB, CLI 등) 간 통신을 담당합니다.
- 코어 내부에 특정한 레이어를 엄격하게 강제하지 않으며, 모듈 하나로 유지할 수도 있습니다.
- 핵심은 의존성 역전과 외부 시스템과의 유연한 통합에 초점이 맞춰져 있습니다.
-
클린 아키텍처
- 헥사고날 아키텍처, 어니언 아키텍처, DDD를 통합하여 발전된 구조입니다.
- 명확한 동심원 레이어 구조를 갖습니다: 엔티티(도메인), 유스케이스(애플리케이션 로직), 인터페이스 어댑터, 프레임워크/드라이버.
- 의존성의 방향을 엄격히 정의하며, 프레임워크 → 어댑터 → 애플리케이션 → 도메인 구조로만 의존성이 흐릅니다.
- 코어를 더 세밀하게 쪼개어 도메인 로직과 유스케이스를 명확히 분리합니다.
2. 모듈화 및 초점
-
헥사고날 아키텍처
- 코어 독립성과 테스트 용이성, 외부 어댑터 교체에 중점.
- 코어 내부 구조는 비교적 자유로워 마이크로서비스나 이벤트 중심 시스템에 적합.
- 다양한 포트와 어댑터를 쉽게 추가할 수 있고, 도메인과 유스케이스 로직 분리가 덜 엄격할 수 있습니다.
-
클린 아키텍처
- 엄격한 레이어 분리를 통한 모듈화 강화.
- 도메인 로직, 비즈니스 유스케이스, 프레젠테이션, 인프라를 명확히 구분.
- 대규모 시스템에 적합, 테스트 가능성과 유지보수성도 높음.
3. 유연성 대 구조화
- 헥사고날 아키텍처는 빠르게 적용 가능한 심플한 구조로, 디커플링(de-coupling)과 인터페이스 기반 통합에 더 중점을 둡니다.
- 클린 아키텍처는 더 강력한_rules_과_계층 구분_을 적용해 확장성, 협업, 유지보수가 필요한 대형 프로젝트에 적합합니다.
요약 표
| 평가 항목 | 헥사고날 아키텍처 | 클린 아키텍처 |
|---|---|---|
| 레이어 구조 | 엄격한 레이어 구조 없음 | 명확한 동심원 레이어 구조 |
| 의존성 방향 | 코어 <-> 포트/어댑터 | 엄격히 안쪽으로만 의존 |
| 코어 조직 | 상대적으로 자유로움 | 도메인, 유스케이스 등으로 세분화 |
| 유연성 | 단순하고 빠름 | 구조적, 유지보수·확장 용이 |
| 적용 대상 | 마이크로서비스, 다양한 통합 환경 | 대규모, 복잡하고 모듈화 필요한 시스템 |
| 초점 | 코어 독립성 및 통합 가변성 | 모듈화, 테스트 용이성 및 규칙 준수 |
| 테스트 용이성 | 매우 높음 | 매우 높음 |
결론
클린 아키텍처는 헥사고날 아키텍처에 내부 레이어링과 의존성 방향 규칙을 더 명확히 한 발전형으로 볼 수 있습니다. 헥사고날은 핵심 비즈니스 로직과 외부 통합을 인터페이스로 분리해 단순하고 유연한 반면, 클린은 이 모델을 확장해 규모 있는 시스템에서도 관리 및 확장이 쉽도록 설계되었습니다.
references